﻿namespace IronLua.Parsing

open System
open System.Text
open System.Text.RegularExpressions
open System.Globalization
open System.Collections.Generic
open Utilities

type RegexMatchAction = string -> string -> Match -> TokenPosition -> TokenMatch option

type RegexLexer() =

    static let newLineRegex = new Regex("\r\n|\n|\r", RegexOptions.Multiline ||| RegexOptions.Compiled)
    static let newLineExpr = @"\r\n|\r|\n"
    static let whitespaceExpr = @"(\t|\v|\f|\040)+"
    static let booleanExpr = @"\b(true|false)"
    static let nilExpr = @"nil"
    static let idExpr = @"[_A-Za-z][_A-Za-z0-9]*"
    static let numberExpr = @"([0-9]*\.)?[0-9]+([eE][-+]?[0-9]+)?"
    static let hexNumberExpr = @"0[xX][0-9a-fA-F]+(\.[0-9a-fA-F])?([pP][-\+]?[0-9])?"
    static let separatorExpr = @"\{|\}|\(|\)|\[|\]|,|;|\|"
    static let varArgsExpr = @"\.\.\."
    static let operatorExpr = @"\.\.|\.|::|@|:|\|\||&&|\^|#|\+=|-=|\*=|/=|==|\<=|\>=|=|!=|~=|\+|\-|\*|//|/|%|\<|\>|\band\b|\bor\b|\bnot\b"
    static let stringExpr = @"'(?<string>[^'\\\n\r]*(\\.[^'\\]*)*)'|""(?<string>[^""\\\n\r]*(\\.[^""\\]*)*)"""
    static let longStringExpr = @"\[(?<eq>=*)\[(?<string>[\s\S]*?)\](\k<eq>)\]"
    static let commentExpr = @"--(?<comment>.*)(\r\n|\n|\r)?"
    static let longCommentExpr = @"--\[(?<eq>=*)\[(?<comment>[\s\S]*?)\](\k<eq>)\]"

    static let newLineMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        Some(TokenMatch.Consume(TokenPosition(newIndex, pos.Line + 1, 1, 0)))

    static let whitespaceMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        Some(TokenMatch.Consume(TokenPosition(newIndex, pos.Line, pos.Column + value.Length, 0)))    

    static let booleanMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = TokenPosition(newIndex, pos.Line, pos.Column + value.Length, 0)
        match value with
        | "true" ->     Some(TokenMatch.Success(Token(TokenType.True, pos, true), newPos))
        | "false" ->    Some(TokenMatch.Success(Token(TokenType.False, pos, false), newPos))
        | _ ->          None

    static let nilMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = TokenPosition(newIndex, pos.Line, pos.Column + value.Length, 0)
        match value with
        | "nil" ->  Some(TokenMatch.Success(Token(TokenType.Nil, pos), newPos))
        | _ ->      None

    static let idOrKeywordMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let id = value
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        match id with 
        | "do" | "end" | "local" | "function"
        | "if" | "then" | "else" | "elseif" | "for" | "in"
        | "while" | "repeat" | "until" | "break" | "goto"
        | "return" ->   Some(TokenMatch.Success(Token(TokenType.Keyword, pos, id), newPos))
        | otherwise ->  Some(TokenMatch.Success(Token(TokenType.Identifier, pos, id), newPos))
    
    static let numberMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let style = NumberStyles.Float
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        match System.Double.TryParse(value, style, CultureInfo.InvariantCulture) with
        | true, num ->  Some(TokenMatch.Success(Token(TokenType.Number, pos, num), newPos))
        | false, _ ->   None

    static let hexNumberMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        let num = System.Convert.ToUInt64(value, 16) |> double
        Some(TokenMatch.Success(Token(TokenType.Number, pos, num), newPos))   

    static let separatorMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        Some(TokenMatch.Success(Token(TokenType.Separator, pos, value), newPos)) 

    static let varArgsMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        Some(TokenMatch.Success(Token(TokenType.VarArgs, pos, value), newPos))

    static let operatorMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line, pos.Column + m.Length, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        Some(TokenMatch.Success(Token(TokenType.Operator, pos, value), newPos))

    static let stringMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let escapeLuaString str = str
        let findNewLineCol str = 
            let countNewLines = newLineRegex
            let matches = countNewLines.Matches(str)
            match matches.Count with 
            | x when x > 0 ->   (x, str.Length - matches.[x - 1].Index - 1)            
            | count ->          (count, str.Length)
        let str = m.Groups.["string"].Value
        let newLines, col = findNewLineCol value
        let index = pos.Index + value.Length
        let newPos = 
            if newLines > 0 then
                new TokenPosition(index, pos.Line + newLines, col, 0)
            else
                new TokenPosition(index, pos.Line, pos.Column + value.Length, 0)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        Some(TokenMatch.Success(Token(TokenType.String, pos, escapeLuaString str), newPos))

    static let longStringMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        stringMatch src value m pos

    static let commentMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newIndex = pos.Index + value.Length
        let newPos = new TokenPosition(newIndex, pos.Line + 1, 1, value.Length)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        let comment = m.Groups.["comment"].Value.Trim()
        Some(TokenMatch.Consume(newPos))

    static let longCommentMatch (src : string) (value : string) (m : Match) (pos : TokenPosition) =
        let newLines, col = findNewLineCol value
        let index = pos.Index + value.Length
        let newPos = new TokenPosition(index, pos.Line + newLines, col, 0)
        let pos = new TokenPosition(pos.Index, pos.Line, pos.Column, value.Length)
        let str = m.Groups.["comment"].Value.Trim()
        Some(TokenMatch.Consume(newPos))

    static let luaTokens = // order matters
                    [   ("NewLine",             newLineExpr,       newLineMatch);
                        ("Whitespace",          whitespaceExpr,    whitespaceMatch);                        
                        ("LongComment",         longCommentExpr,   longCommentMatch);
                        ("Comment",             commentExpr,       commentMatch);
                        ("Nil",                 nilExpr,           nilMatch);
                        ("Boolean",             booleanExpr,       booleanMatch);
                        ("HexNumber",           hexNumberExpr,     hexNumberMatch);
                        ("Number",              numberExpr,        numberMatch);
                        ("VarArgs",             varArgsExpr,       varArgsMatch);
                        ("String",              stringExpr,        stringMatch);
                        ("LongString",          longStringExpr,    longStringMatch);
                        ("Separator",           separatorExpr,     separatorMatch);
                        ("Operator",            operatorExpr,      operatorMatch);
                        ("IdentifierOrKeyword", idExpr,            idOrKeywordMatch); ]

    static let luaRegex = 
        let regexFolder str def =     
            let (name, pattern, handler) =          def
            let groupedExpr =                       sprintf @"(?<%s>%s)" name pattern
            if String.IsNullOrWhiteSpace(str) then  groupedExpr 
            else                                    str + "|" + groupedExpr            
        let completeExpr = luaTokens |> List.fold regexFolder "" |> string
        new Regex(completeExpr, RegexOptions.Compiled);

    static let tokenize (regex : Regex) tokendefs (src : string) : Token seq = 
        let checkBoundsWhen fn (pos : TokenPosition) =
            if pos.Index >= src.Length then None
            else                            fn(pos)
        let tokenizing (pos : TokenPosition) =
            let index = pos.Index
            let line = pos.Line
            let col = pos.Column
            match regex.Match(src, index) with
            | m when m.Success && m.Index = index ->
                let matchedDef = tokendefs |> List.find (fun def -> let grp, _, _ = def in m.Groups.[grp |> string].Length > 0) 
                let groupName, _, onMatch = matchedDef
                match (onMatch src m.Value m pos) with
                | Some(tokenMatch) ->   match tokenMatch with
                                        | Consume(newpos) ->        Some(None, newpos)
                                        | Success(token, newpos) -> Some(Some(token), newpos)

                | None ->               lexfailure src pos
            | otherwise -> 
                lexfailure src pos

        let takeLastAndAppendEos tseq : Token option seq = 
            let lastToken = Seq.last tseq
            match lastToken with
            | Some(last) -> Seq.append tseq ([| Some(last |> Token.MakeEos) |] |> Array.toSeq)
            | None ->       [| Some(Token(TokenType.EOS, TokenPosition.Empty)) |] |> Array.toSeq

        TokenPosition.Empty 
        |> Seq.unfold   (checkBoundsWhen tokenizing)
        |> Seq.filter   Option.isSome
        |> takeLastAndAppendEos
        |> Seq.map      Option.get 

    static member Tokenize src = src |> tokenize luaRegex luaTokens



